// Copyright 2011 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * A visitor to detect goog.scope() wrappers.  Determines the aliases used in a
 * given JS source file and sets them on the DocInfo object so later uses
 * can be detected and names properly expanded.
 *
 * @see https://docs.google.com/document/pub?id=1ETFAuh2kaXMVL-vafUYhaWlhl6b5D9TOvboVg7Zl68Y&pli=1
 */

var logger = require('ringo/logging').getLogger('GoogScopeV');


/**
 * Visitor designed to look for SCRIPT nodes, determine if they contain
 * goog.scope wrappers, and if so, save the aliases onto the DocInfo object
 * so they can be expanded later.
 *
 * @constructor
 */
var GoogScopeV = exports.GoogScopeV = function(verbose) {
  /**
   * The next visitor in the chain.
   *
   * @type {Documenter.Visitor}
   * @private
   */
  this.nextVisitor_ = null;

  /**
   * Print lots of logging output?
   *
   * @type {boolean}
   * @private
   */
  this.verbose_ = verbose;
};


/**
 * @param {Documenter.Visitor} visitor The next visitor in the chain.
 */
GoogScopeV.prototype.setNext = function (visitor) {
  this.nextVisitor_ = visitor;
};


/**
 * Determine if a node is a script block and, if so, determine if the script
 * has a goog.scope wrapper.
 *
 * @param {Node} node The node to visit.
 * @parma {DocInfo} docInfo The page's documentation info object.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 */
GoogScopeV.prototype.visit = function visit(node, docInfo, jsdoc) {
  if (node.isScript()) {
    var wrapper = findGoogScope(node),
        findings = wrapper ? extractAliases(wrapper) : {count: 0};

    if (findings.count > 0) {
      var aliases = findings.aliases;

      if (this.verbose_) {
        logger.info('goog.scope Aliases in File: ' + docInfo.filename);
        var shortNames = Object.keys(aliases);
        for (var i = 0, len = shortNames.length; i < len; ++i) {
          var shortName = shortNames[i],
              fullName = aliases[shortName];
          logger.info('\t' + shortName + ' => ' + fullName);
        }
      }

      docInfo.setGoogScopeAliases(aliases);
    };
  }

  if (this.nextVisitor_) {
    this.nextVisitor_.visit(node, docInfo, jsdoc);
  };
};


/**
 * goog.scope wrappers must appear immediately after the last goog.provide/
 * goog.require call.  So if you have a SCRIPT node, you can iterate through
 * the children looking for the EXPR_RESULT with a CALL to goog.scope.
 * if anything is found other than goog.provide and good.require calls,
 * abort.
 *
 * @param {Node} node The SCRIPT node to be evaluated.
 * @return {(Node|null)} If a goog.scope call is found, the Node representing
 *    the anonymous function wrapper will be returned.  Otherwise, null.
 */
function findGoogScope(node) {
  /*
    Example AST;
    
    SCRIPT
      EXPR_RESULT
        CALL
          GETPROP
            NAME goog
            STRING require
          STRING goog.dom
      EXPR_RESULT             // children[1]
        CALL                  // grandkid
          GETPROP             // getQualifiedName() => funcName
            NAME goog
            STRING scope
          FUNCTION            // This is the node to return, if present.
  */
  
  var wrapper = null,
      children = node.children().iterator();

  while (children.hasNext()) {
    var child = children.next();

    if (child.isExprResult()) {
      var grandkid = child.getFirstChild();

      if (grandkid.isCall()) {
        var callee = grandkid.getFirstChild(),
            funcName = callee.getQualifiedName();

        if (funcName === 'goog.provide' || funcName === 'goog.require') {
          continue;
        }
        else if (funcName === 'goog.scope') {
          wrapper = grandkid.getChildAtIndex(1);

          // Found it.  Quit and return.
          break;
        }
        else {
          // If we find anything that's not a call to goog.provide or
          // goog.require before finding goog.scope, we're never gong to find
          // it.
          break;
        }
        
      }
      else {
        // If we find anything that's not a call to goog.provide or
        // goog.require before finding goog.scope, we're never gong to find
        // it.
        break;
      }
    }
    else {
      // If anything but an ExprResult appears, then it's illegal for goog.scope
      // to come after.  Therefore, there isn't one.
      break;
    }
  }
  
  return wrapper;
}


/**
 * Extract the aliases used within a goog.scope wrapper.
 *
 * @param {Node} wrapper The FUNCTION Node representing the wrapper passed to
 *    goog.scope.
 * @return {{count: number, aliases: Object.<string, string>}} Object
 *    containing the number of aliases found and a map of aliases to their
 *    fully qualified names.
 */
function extractAliases(wrapper) {
  /*
    According to the style guide, all alias assignments must appear
    at the top of the wrapper function, so scrape the function's AST for
    leading "var" statements that are assigned something with a fully
    qualified name.

    Also according to the style guide, aliases must appear in alphabetical
    order and be named the same as the last property in the fully qualified
    name.  Unfortunately, the only example of goog.scope in the Closure Lib
    does not follow these rules (as of rev 1647).

    Example AST, showing multiple single-variable VAR statements.  If multiple
    names are used under one VAR, all NAME elements have the same VAR parent.
 
    FUNCTION
      NAME
      PARAM_LIST
      BLOCK
        VAR
          NAME createElement
            GETPROP
              GETPROP
                NAME goog
                STRING dom
              STRING createElement
        VAR 
          NAME createElement
            GETPROP
              GETPROP
                NAME goog
                STRING dom
              STRING createElement
  */

  var count = 0,
      aliases = Object.create(null),
      block = wrapper.getChildAtIndex(2),
      children = block.children().iterator();

  var varExpr,
      done = false;
  while (children.hasNext() &&
         (varExpr = children.next()) &&
         varExpr.isVar() &&
         ! done) {

    var aliasDefs = varExpr.children().iterator();

    while (aliasDefs.hasNext()) {
      var def = aliasDefs.next(),
          alias = def.getString(),
          rhs = def.getFirstChild();

      if (! rhs.isQualifiedName()) {
        done = true;
        break;
      }

      ++count;
      aliases[alias] = rhs.getQualifiedName();
    }
  }

  return {
    count: count,
    aliases: aliases
  };
}
